package com.zf.minio.config;


import com.zf.minio.vo.DownloadFile;
import com.zf.minio.vo.FileInfo;
import com.zf.minio.vo.FileStatInfo;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * minio客户端工具类
 */
@Slf4j
public class MinioClientUtils {

    /**
     * 文件分片大小
     */
    public static final int PART_SIZE = 5 * 1024 * 1024;

    private static MinioClient minioClient;
    private static MinioConfig minioConfig;

    /**
     * 初始化Minio客户端工具
     */
    public static void init(MinioConfig minioConfig) {
        MinioClientUtils.minioConfig = minioConfig;
        MinioClientUtils.minioClient = MinioClient.builder()
                .endpoint(minioConfig.getEndpoint())
                .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey())
                .build();
    }

    /**
     * 如果 bucket（存储桶） 不存在就创建
     *
     * @param bucketName bucket的名称
     */
    @SneakyThrows
    public static void createBucketIfAbsent(String bucketName) {
        BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
        // 判断存储桶是否存在
        boolean exists = minioClient.bucketExists(bucketExistsArgs);
        if (!exists) {
            // 创建存储桶
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());

            // 设置存储桶的策略为公开 PUBLIC
            SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs
                    .builder()
                    .bucket(bucketName)
                    .config(publicBucketPolicy(bucketName))
                    .build();
            minioClient.setBucketPolicy(setBucketPolicyArgs);
        }
    }

    /**
     * 列出所有的桶
     */
    @SneakyThrows
    public static List<String> listBuckets() {
        List<Bucket> list = minioClient.listBuckets();
        ArrayList<String> res = new ArrayList<>();
        if (CollectionUtils.isEmpty(list)) {
            return res;
        }
        for (Bucket bucket : list) {
            res.add(bucket.name());
        }
        return res;
    }

    /**
     * 删除存储桶
     *
     * @param bucketName bucket的名称
     */
    @SneakyThrows
    public static void removeBucket(String bucketName) {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 上传文件到默认的初始化存储桶[minio.default-bucket-name]中
     *
     * @param multipartFile 表单中的文件对象
     */
    @SneakyThrows
    public static FileInfo uploadToDefaultBucket(MultipartFile multipartFile) {
        String originalFilename = Optional.ofNullable(multipartFile.getOriginalFilename()).orElse("");
        // 文件后缀名
        String suffix = "";
        int index = originalFilename.lastIndexOf('.');
        if (index != -1) {
            suffix = originalFilename.substring(index + 1);
        }
        String uuid = UUID.randomUUID().toString();
        // 生成新的文件名，上传后会自动根据 / 分割创建文件夹
        String filename = String.format("%s/%s.%s", DateTimeFormatter.ofPattern("yyyy/MM/dd").format(LocalDate.now()), uuid, suffix);

        try (InputStream inputStream = multipartFile.getInputStream()) {
            // 构造上传参数
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(minioConfig.getDefaultBucketName())
                    .object(filename)
                    // 文件大小和分片大小，-1表示默认按照5MB分片
                    // 如果对象大小未知，则传递-1给objectSize，并传递有效的partSize。
                    .stream(inputStream, -1, PART_SIZE)
                    .contentType(multipartFile.getContentType())
                    .build();
            // 上传文件
            ObjectWriteResponse objectWriteResponse = minioClient.putObject(putObjectArgs);

            // 返回文件名和访问路径
            FileInfo fileInfo = new FileInfo();
            fileInfo.setFilename(filename);
            fileInfo.setBucketName(minioConfig.getDefaultBucketName());

            // 获取分享文件后的访问地址
            String presignedObjectUrl = getPresignedObjectUrl(minioConfig.getDefaultBucketName(), filename);
            // 由于我们设置了存储桶是公开的，所以只取 生成的URL 中 ? 之前的地址即可，也就是丢弃参数部分
            String fileUrl = presignedObjectUrl.substring(0, presignedObjectUrl.indexOf("?"));
            fileInfo.setUrl(fileUrl);
            return fileInfo;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw e;
        }
    }

    /**
     * 下载文件，
     * 传入 “endpoint/bucket/filename” 中的 filename
     * 返回文件的二进制及类型
     */
    @SneakyThrows
    public static DownloadFile downloadFileFromDefaultBucket(String filename, HttpServletResponse response) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(minioConfig.getDefaultBucketName())
                .object(filename)
                .build();
        // 获取文件，GetObjectResponse 继承了 FilterInputStream
        GetObjectResponse getObjectResponse = minioClient.getObject(getObjectArgs);
        DownloadFile downloadFile = new DownloadFile();
        downloadFile.setBody(getObjectResponse.readAllBytes());
        downloadFile.setContentType(getObjectResponse.headers().get(HttpHeaders.CONTENT_TYPE));
        getObjectResponse.close();
        return downloadFile;
    }

    /**
     * 复制文件
     *
     * @param sourceBucket 原始文件所在的桶
     * @param sourceFile   原始文件名
     * @param targetBucket 目标桶
     * @param targetFile   复制后文件的名称
     */
    @SneakyThrows
    public static void copyFile(String sourceBucket, String sourceFile,
                                String targetBucket, String targetFile) {
        createBucketIfAbsent(targetBucket);
        // 源
        CopySource copySource = CopySource.builder()
                .bucket(sourceBucket)
                .object(sourceFile)
                .build();
        // 目标
        CopyObjectArgs copyArgs = CopyObjectArgs.builder()
                .bucket(targetBucket)
                .object(targetFile)
                .source(copySource)
                .build();
        minioClient.copyObject(copyArgs);
    }

    /**
     * 获取文件统计信息，如最后一次修改时间、大小、
     */
    @SneakyThrows
    public static FileStatInfo getFileStatInfoFromDefaultBucket(String filename) {
        StatObjectArgs statObjectArgs = StatObjectArgs.builder()
                .bucket(minioConfig.getDefaultBucketName())
                .object(filename)
                .build();
        StatObjectResponse statObjectResponse = minioClient.statObject(statObjectArgs);
        FileStatInfo fileStatInfo = new FileStatInfo();
        fileStatInfo.setSize(statObjectResponse.size());
        fileStatInfo.setEtag(statObjectResponse.etag());
        fileStatInfo.setDeleteMarker(statObjectResponse.deleteMarker());
        fileStatInfo.setLastModified(statObjectResponse.lastModified());
        fileStatInfo.setUserMetadata(statObjectResponse.userMetadata());
        return fileStatInfo;
    }

    /**
     * 从默认的存储桶中删除文件,
     * 传入 “endpoint/bucket/filename” 中的 filename
     */
    @SneakyThrows
    public static void deleteFromDefaultBucket(String filename) {
        RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
                .bucket(minioConfig.getDefaultBucketName())
                .object(filename)
                .build();
        minioClient.removeObject(removeObjectArgs);
    }

    /**
     * 从默认的存储桶中批量删除文件,
     * 传入 “endpoint/bucket/filename” 中的 filename
     */
    @SneakyThrows
    public static void batchDeleteFromDefaultBucket(List<String> filenameList) {
        if (CollectionUtils.isEmpty(filenameList)) {
            return;
        }
        // 构建批量删除参数
        List<DeleteObject> deleteObjects = filenameList.stream().map(DeleteObject::new).collect(Collectors.toList());
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(minioConfig.getDefaultBucketName())
                .objects(deleteObjects)
                .build();
        Iterable<Result<DeleteError>> errMsgList = minioClient.removeObjects(removeObjectsArgs);
        for (Result<DeleteError> errMsg : errMsgList) {
            DeleteError deleteError = errMsg.get();
            log.error(deleteError.message());
        }
    }

    /**
     * 列出存储桶中的所有文件
     */
    @SneakyThrows
    public static List<FileInfo> listAllFile(String bucketName) {
        ListObjectsArgs listObjectsArgs = ListObjectsArgs.builder()
                .bucket(bucketName)
                // 递归查询，遇到目录就查询目录中的内容，只返回文件（目录名+文件名，也就是写入桶时的文件名）不返回目录
                .recursive(true)
                .build();
        // 查询桶中的所有文件
        Iterable<Result<Item>> allObjects = minioClient.listObjects(listObjectsArgs);
        ArrayList<FileInfo> res = new ArrayList<>();
        for (Result<Item> fileObj : allObjects) {
            Item item = fileObj.get();
            // 跳过文件夹，如果recursive(true)后，就不会再返回文件
            if (item.isDir()) {
                continue;
            }
            String filename = item.objectName();
            String presignedObjectUrl = getPresignedObjectUrl(bucketName, filename);
            String url = presignedObjectUrl.substring(0, presignedObjectUrl.indexOf("?"));
            FileInfo fileInfo = new FileInfo();
            fileInfo.setUrl(url);
            fileInfo.setFilename(filename);
            fileInfo.setBucketName(bucketName);
            res.add(fileInfo);
        }
        return res;
    }


    /**
     * 为存储桶生成公开策略 PUBLIC，也可以在Minio控制台手动设置
     * <p>
     * 新建的存储桶默认是 PRIVATE，如果直接通过 “endpoint/bucket/filename” 访问文件会被拒绝访问 Access Denied。
     * 设置存储桶的策略为公开后就可以通过  “endpoint/bucket/filename” 来访问了
     * <p>
     * 参考：https://cloud.tencent.com/developer/article/2145878
     *
     * @param bucketName 存储桶名
     */
    public static String publicBucketPolicy(String bucketName) {
        //
        return "{" +
                "  \"Version\":\"2012-10-17\"," +
                "  \"Statement\":[" +
                "    {" +
                "      \"Effect\":\"Allow\"," +
                "      \"Principal\":{" +
                "        \"AWS\":[\"*\"]" +
                "      }," +
                "      \"Action\":[" +
                "        \"s3:ListBucketMultipartUploads\"," +
                "        \"s3:GetBucketLocation\"," +
                "        \"s3:ListBucket\"" +
                "      ]," +
                "      \"Resource\":[" +
                "        \"arn:aws:s3:::" + bucketName + "\"" +
                "      ]" +
                "    }," +
                "    {" +
                "      \"Effect\":\"Allow\"," +
                "      \"Principal\":{" +
                "        \"AWS\":[\"*\"]" +
                "      }," +
                "      \"Action\":[" +
                "        \"s3:ListMultipartUploadParts\"," +
                "        \"s3:PutObject\"," +
                "        \"s3:AbortMultipartUpload\"," +
                "        \"s3:DeleteObject\"," +
                "        \"s3:GetObject\"" +
                "      ]," +
                "      \"Resource\":[" +
                "        \"arn:aws:s3:::" + bucketName + "/*\"" +
                "      ]" +
                "    }" +
                "  ]" +
                "}";
    }

    /**
     * 则新建的存储桶默认是 PRIVATE，如果直接通过 “endpoint/bucket/filename” 访问文件会被拒绝访问 Access Denied。
     * - 可以指定存储桶的为 PUBLIC。
     * <p>
     * - 也可以通过如下方式生成一个给HTTP GET请求用的 Presigned URL（其实就是 “endpoint/bucket/filename?一些参数”）。可以用这个URL进行下载和浏览文件，即使文件所在的存储桶是 PRIVATE，
     * <p>
     * 但是这种方式生成的URL是有过期时间的，一段时间后就不能在访问了
     */
    public static String getPresignedObjectUrl(String bucketName, String objectName) throws Exception {
        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                // .expiry() // 设置URL的过期时间，单位秒，默认是7天，最大不能超过7天，最小是1s,
                .method(Method.GET)
                .build();
        return minioClient.getPresignedObjectUrl(build);
    }

    public static MinioClient getMinioClient() {
        return minioClient;
    }

    public static MinioConfig getMinioConfig() {
        return minioConfig;
    }
}
